﻿/*=================================================================================================
// <copyright company="AltaModa Technologies, LLC">
//	Copyright (c) AltaModa Technologies, LLC. All rights reserved.
//	http://www.AltaModaTech.com/
// </copyright>
// 
//	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
//	IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 
//	AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
//	CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
//	CONSEQUENTIAL DAMAGES INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
//	SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
//	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
//	OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
//	POSSIBILITY OF SUCH DAMAGE.
//===============================================================================================*/

using System;
using System.Xml;
using System.Text;

namespace AMT.Web.Security
{
	public class ConfigFileRoleProvider : DenyAllRoleProvider
	{
		#region Private static, const members
		private static string[] s_EmptyStringArray = new string[] { };
		private const string PROVIDER_CONFIG_ATTRIBITE_NAME = "providerConfigFilePath";
		private const string DEFAULT_PROVIDER_CONFIG_FILE_PATH = "~/Account/MemberRoles.xml";
		#endregion

		#region Private data members
		private string _providerConfigFilePath;
		private string[] _uniqueRoleNames;
		private System.Collections.Specialized.StringDictionary _userRoles;
		private System.Collections.Specialized.StringDictionary _roleUsers;
		#endregion

		#region RoleProvider implementation
		public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
		{
			base.Initialize(name, config);

			// Get the setting for the xml file's location
			if (null != config[PROVIDER_CONFIG_ATTRIBITE_NAME])
			{
				_providerConfigFilePath = config[PROVIDER_CONFIG_ATTRIBITE_NAME];
			}

			// Use default file path if none was specified in the attribute
			if (null == _providerConfigFilePath || 0 == _providerConfigFilePath.Length)
			{
				_providerConfigFilePath = DEFAULT_PROVIDER_CONFIG_FILE_PATH;
			}

			InitializeRoleData();
		}

		public override string Description
		{
			get
			{
				return "AMT.Web.Security.Debug.ConfigFileRoleProvider - DO NOT use in production systems";
			}
		}

		// TODO: restrict role membership to xml sections matching this app name
		/// <summary>
		/// Retains any string set as the application name
		/// </summary>
		public override string ApplicationName { get; set; }

		// TODO: support wildcards in usernameToMatch
		// TODO: return user names in alpha order per spec (http://msdn.microsoft.com/en-us/library/system.web.security.roleprovider.findusersinrole.aspx)
		public override string[] FindUsersInRole(string roleName, string usernameToMatch)
		{
			string[] retval = s_EmptyStringArray;

			// TODO: replace this with wildcard supporting impl'n. For now, just return the user name if they're in the role
			if (IsUserInRole(usernameToMatch, roleName))
				retval = new string[] { usernameToMatch };

			return retval;
		}

		public override string[] GetAllRoles()
		{
			return _uniqueRoleNames;
		}

		public override string[] GetRolesForUser(string username)
		{
			string[] retval = s_EmptyStringArray;

			if (_userRoles.ContainsKey(username))
			{
				retval = _userRoles[username].Split(',');
			}

			return retval;
		}

		public override string[] GetUsersInRole(string roleName)
		{
			string[] retval = s_EmptyStringArray;

			if (_roleUsers.ContainsKey(roleName))
			{
				retval = _roleUsers[roleName].Split(',');
			}

			return retval;
		}

		public override bool IsUserInRole(string username, string roleName)
		{
			bool isInRole = false;

			foreach (string userInRole in GetUsersInRole(roleName)) {
				if (0 == String.Compare(userInRole, username, true))
				{
					isInRole = true;
					break;
				}
			}

			return isInRole;
		}

		public override bool RoleExists(string roleName)
		{
			return _roleUsers.ContainsKey(roleName);
		}
		#endregion


		#region Private methods
		private void InitializeRoleData()
		{
			_userRoles = LoadUserRolesFromConfig();
			_uniqueRoleNames = GetUniqueRoles(_userRoles);
			_roleUsers = GetRoleUsers(_uniqueRoleNames);
		}

		private System.Collections.Specialized.StringDictionary GetRoleUsers(string[] uniqueRoleNames)
		{
			System.Collections.Specialized.StringDictionary roleUsers = new System.Collections.Specialized.StringDictionary();
			StringBuilder csvBldr = new StringBuilder(1024);

			// Walk through unique role names
			foreach (string rolename in uniqueRoleNames)
			{
				csvBldr.Length = 0; // clear existing
				// Walk through user names
				foreach (string username in _userRoles.Keys)
				{
					// If the current role name is in this user's role list, add the user name to csv list
					if (ValueIsInCSV(rolename, _userRoles[username]))
					{
						csvBldr.AppendFormat("{0},", username);
					}
				}

				// TODO: if no users are in this role, should it be added to the output dictionary?
				// Add the csv list of users for the current role name
				roleUsers.Add(rolename, NormalizeCSVString(csvBldr.ToString()));
			}

			return roleUsers;
		}

		// TODO: should this use case insensitive comparison?
		private bool ValueIsInCSV(string searchValue, string csv)
		{
			bool isInCsv = false;

			foreach (string s in csv.Split(','))
			{
				if (searchValue == s)
				{
					isInCsv = true;
					break;
				}
			}

			return isInCsv;
		}

		private string[] GetUniqueRoles(System.Collections.Specialized.StringDictionary userRoles)
		{
			string[] rgRoles = s_EmptyStringArray;

			if (null != userRoles && 0 < userRoles.Count)
			{
				System.Collections.Specialized.StringDictionary d = new System.Collections.Specialized.StringDictionary();

				StringBuilder bldr = new StringBuilder(1024);

				foreach (string roles in userRoles.Values)
				{
					foreach (string role in roles.Split(','))
					{
						try
						{
							d.Add(role, null);
						}
						catch (ArgumentException)
						{
							// Ignore this exception which occurs when a duplicate key is encountered
						}
					}
				}

				rgRoles = new string[d.Keys.Count];
				d.Keys.CopyTo(rgRoles, 0);
			}
			return rgRoles;
		}
		#endregion


		// TODO: use shared code
		#region Private methods
		private System.Collections.Specialized.StringDictionary LoadUserRolesFromConfig()
		{

			// TODO: consider caching after loading; but consider dev need to make config changes at runtime
			System.Collections.Specialized.StringDictionary userRolesFromXml = new System.Collections.Specialized.StringDictionary();

			var xml = LoadXmlConfigFile(_providerConfigFilePath);

			// TODO: consider adding restriction by app name to support same user having diff roles in diff apps
			foreach (XmlNode node in xml.SelectNodes("/application/user"))
			{
				userRolesFromXml.Add(node.Attributes["name"].Value, NormalizeCSVString(node.Attributes["roles"].Value));
			}

			return userRolesFromXml;
		}

		// TODO: moved to shared lib?
		private XmlDocument LoadXmlConfigFile(string filePath)
		{
			System.Diagnostics.Debug.Assert(null != filePath && 0 < filePath.Length);

			if (System.Web.VirtualPathUtility.IsAppRelative(filePath))
			{
				filePath = System.Web.HttpContext.Current.Server.MapPath(filePath);
			}

			var xml = new XmlDocument();
			xml.Load(filePath);
			return xml;
		}

		internal string NormalizeCSVString(string csvIn)
		{
			string retval = string.Empty;

			if (null != csvIn && 0 < csvIn.Length)
			{
				StringBuilder bldr = new StringBuilder(1024);
				foreach (string substring in csvIn.Split(','))
				{
					// If the substring has characters other than spaces...
					if (0 < substring.Trim().Length)
					{
						//... keep it in a CSV string
						bldr.AppendFormat("{0},", substring.Trim());
					}
				}

				// If any values were stored in the new CSV string, remove the trailing comma
				if (0 < bldr.Length)
				{
					bldr.Remove(bldr.Length - 1, 1);
				}

				retval = bldr.ToString();
			}

			return retval;
		}

		#endregion

	}
}
